/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.ymt.spi.webserver.tomcat.realm;


import java.security.Principal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;

import javax.naming.Context;
import javax.sql.DataSource;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.realm.RealmBase;
import org.apache.naming.ContextBindings;

/**
*
* Implementation of <b>Realm</b> that works with any JDBC JNDI DataSource.
* See the JDBCRealm.howto for more details on how to set up the database and
* for configuration options.
*
* @author Johnny
* @version For tomcat 7.0.73
*/

public class DataSourceRealm
    extends RealmBase {


    // ----------------------------------------------------- Instance Variables

    /**
     * The generated string for the roles PreparedStatement
     */
    private String preparedSql = null;

	/**
     * The name of the JNDI JDBC DataSource
     */
    protected String dataSourceName = null;


    /**
     * Descriptive information about this Realm implementation.
     */
    protected static final String info =
        "org.apache.catalina.realm.DataSourceRealm/1.0";


    /**
     * Context local datasource.
     */
    protected boolean localDataSource = false;


    /**
     * Descriptive information about this Realm implementation.
     */
    protected static final String name = "DataSourceRealm";


    /**
     * The column in the user role table that names a role
     */
    protected String tenantIdCol = null;
    
	/**
     * The column in the user role table that names a role
     */
    protected String jaasRoleNameCol = null;

    /**
     * The class name for generating role select sql
     */
    protected String bizRoleSqlSelectorClass = null;
    
	/**
     * The class for generating role select sql
     */
    protected BizRoleSqlSelector bizRoleSqlSelector;

    /**
     * The column in the user table that holds the user's credentials
     */
    protected String userCredCol = null;


    /**
     * The column in the user table that holds the user's login name, usually a user identifier column in session
     */
    protected String loginNameCol = null;
    /**
     * The column in the user table that holds the user's display name, usually a user name column in session
     */
    protected String displayNameCol = null;
    
    /**
     * The column in the user table that holds the user's name
     */
    protected String userNameCol1 = null;
    
	/**
     * The column in the user table that holds the user's name
     */
    protected String userNameCol2 = null;

    /**
     * The table that holds user data.
     */
    protected String userTable = null;

    /**
     * The col that holds user status.
     */
    protected String statusCol = null;

    /**
     * The col that holds user status.
     */
    protected String status = null;


    // ------------------------------------------------------------- Properties


    /**
     * Return the name of the JNDI JDBC DataSource.
     *
     */
    public String getDataSourceName() {
        return dataSourceName;
    }

    /**
     * Set the name of the JNDI JDBC DataSource.
     *
     * @param dataSourceName the name of the JNDI JDBC DataSource
     */
    public void setDataSourceName( String dataSourceName) {
      this.dataSourceName = dataSourceName;
    }

    /**
     * Return if the datasource will be looked up in the webapp JNDI Context.
     */
    public boolean getLocalDataSource() {
        return localDataSource;
    }

    /**
     * Set to true to cause the datasource to be looked up in the webapp JNDI
     * Context.
     *
     * @param localDataSource the new flag value
     */
    public void setLocalDataSource(boolean localDataSource) {
      this.localDataSource = localDataSource;
    }

    public String getTenantIdCol() {
		return tenantIdCol;
	}

	public void setTenantIdCol(String tenantIdCol) {
		this.tenantIdCol = tenantIdCol;
	}

    /**
     * Return the column in the user role table that names a role.
     *
     */
    public String getJaasRoleNameCol() {
        return jaasRoleNameCol;
    }

    /**
     * Set the column in the user role table that names a role.
     *
     * @param jaasRoleNameCol The column name
     */
    public void setJaasRoleNameCol( String jaasRoleNameCol ) {
        this.jaasRoleNameCol = jaasRoleNameCol;
    }

    /**
     * Return the column in the user table that holds the user's credentials.
     *
     */
    public String getUserCredCol() {
        return userCredCol;
    }

    /**
     * Set the column in the user table that holds the user's credentials.
     *
     * @param userCredCol The column name
     */
    public void setUserCredCol( String userCredCol ) {
       this.userCredCol = userCredCol;
    }
    
    public String getLoginNameCol() {
		return loginNameCol;
	}

	public void setLoginNameCol(String loginNameCol) {
		this.loginNameCol = loginNameCol;
	}
	public String getDisplayNameCol() {
		return displayNameCol;
	}

	public void setDisplayNameCol(String displayNameCol) {
		this.displayNameCol = displayNameCol;
	}

	/**
     * Return the column in the user table that holds the user's name.
     *
     */
    public String getUserNameCol1() {
        return userNameCol1;
    }

    /**
     * Set the column in the user table that holds the user's name.
     *
     * @param userNameCol1 The column name
     */
    public void setUserNameCol1( String userNameCol1 ) {
       this.userNameCol1 = userNameCol1;
    }
    
    /**
     * Return the column in the user table that holds the user's name.
     *
     */
    public String getUserNameCol2() {
        return userNameCol2;
    }

    /**
     * Set the column in the user table that holds the user's name.
     *
     * @param userNameCol2 The column name
     */
    public void setUserNameCol2( String userNameCol2 ) {
       this.userNameCol2 = userNameCol2;
    }

    /**
     * Return the table that holds user data..
     *
     */
    public String getUserTable() {
        return userTable;
    }

    /**
     * Set the table that holds user data.
     *
     * @param userTable The table name
     */
    public void setUserTable( String userTable ) {
      this.userTable = userTable;
    }
    
    public String getBizRoleSqlSelectorClass() {
		return bizRoleSqlSelectorClass;
	}

	public void setBizRoleSqlSelectorClass(String bizRoleSqlSelectorClass) {
		this.bizRoleSqlSelectorClass = bizRoleSqlSelectorClass;
	}

    public String getStatusCol() {
        return statusCol;
    }

    public void setStatusCol(String statusCol) {
        this.statusCol = statusCol;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    /**
     * Return descriptive information about this Realm implementation and
     * the corresponding version number, in the format
     * <code>&lt;description&gt;/&lt;version&gt;</code>.
     */
    @Override
    public String getInfo() {

        return info;

    }


    // --------------------------------------------------------- Public Methods


    /**
     * Return the Principal associated with the specified username and
     * credentials, if there is one; otherwise return <code>null</code>.
     *
     * If there are any errors with the JDBC connection, executing
     * the query or anything we return null (don't authenticate). This
     * event is also logged, and the connection will be closed so that
     * a subsequent request will automatically re-open it.
     *
     * @param username Username of the Principal to look up
     * @param credentials Password or other credentials to use in
     *  authenticating this username
     */
    @Override
    public Principal authenticate(String username, String credentials) {

        // No user or no credentials
        // Can't possibly authenticate, don't bother the database then
        if (username == null || credentials == null) {
            return null;
        }

        Connection dbConnection = null;

        // Ensure that we have an open database connection
        dbConnection = open();
        if (dbConnection == null) {
            // If the db connection open fails, return "not authenticated"
            return null;
        }

        try
        {
            // Acquire a Principal object for this user
            return authenticate(dbConnection, username, credentials);
        }
        finally
        {
            close(dbConnection);
        }
    }


    // -------------------------------------------------------- Package Methods


    // ------------------------------------------------------ Protected Methods


    /**
     * Return the Principal associated with the specified username and
     * credentials, if there is one; otherwise return <code>null</code>.
     *
     * @param dbConnection The database connection to be used
     * @param username Username of the Principal to look up
     * @param credentials Password or other credentials to use in
     *  authenticating this username
     */
    protected Principal authenticate(Connection dbConnection,
                                     String username,
                                     String credentials) {
    	
    	
        // No user or no credentials
        // Can't possibly authenticate, don't bother the database then
        if (username == null || credentials == null) {
            if (containerLog.isTraceEnabled())
                containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure",
                                                username));
            return null;
        }
        
        AuthInfo info = getAuthInfo(dbConnection, username);

        if(info==null || info.getDbCredentials() == null) {
            // User was not found in the database.
            // Waste a bit of time as not to reveal that the user does not exist.
            compareCredentials(credentials, getClass().getName());

            if (containerLog.isTraceEnabled())
                containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure",
                                                username));
            return null;
        }

        // Validate the user's credentials
        boolean validated = compareCredentials(credentials, info.getDbCredentials());

        if (validated) {
            if (containerLog.isTraceEnabled())
                containerLog.trace(sm.getString("dataSourceRealm.authenticateSuccess",
                                                username));
        } else {
            if (containerLog.isTraceEnabled())
                containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure",
                                                username));
            return null;
        }

        // Create and return a suitable Principal for this user
        return new TenantPrincipal(info.getDbLoginName(), 
        		credentials, 
        		info.getDbDisplayName(),
        		info.getDbTenantId(), 
        		info.getDbJaasRoles(),
        		info.getDbBizRoles());
    }


    /**
     * Close the specified database connection.
     *
     * @param dbConnection The connection to be closed
     */
    protected void close(Connection dbConnection) {

        // Do nothing if the database connection is already closed
        if (dbConnection == null)
            return;

        // Commit if not auto committed
        try {
            if (!dbConnection.getAutoCommit()) {
                dbConnection.commit();
            }
        } catch (SQLException e) {
            containerLog.error("Exception committing connection before closing:", e);
        }

        // Close this database connection, and log any errors
        try {
            dbConnection.close();
        } catch (SQLException e) {
            containerLog.error(sm.getString("dataSourceRealm.close"), e); // Just log it here
        }

    }

    /**
     * Open the specified database connection.
     *
     * @return Connection to the database
     */
    public Connection open() {

        try {
            Context context = null;
            if (localDataSource) {
                context = ContextBindings.getClassLoader();
                context = (Context) context.lookup("comp/env");
            } else {
                context = getServer().getGlobalNamingContext();
            }
            DataSource dataSource = (DataSource)context.lookup(dataSourceName);
        return dataSource.getConnection();
        } catch (Exception e) {
        	e.printStackTrace();
            // Log the problem for posterity
            containerLog.error(sm.getString("dataSourceRealm.exception"), e);
        }
        return null;
    }

    /**
     * Return a short name for this Realm implementation.
     */
    @Override
    protected String getName() {

        return (name);

    }

    /**
     * Return the roles associated with the given user name
     * @param dbConnection The database connection to be used
     * @param username Username for which roles should be retrieved
     */
    protected AuthInfo getAuthInfo(Connection dbConnection,
                                     String username) {

        //if (allRolesMode != AllRolesMode.STRICT_MODE) {
            // Using an authentication only configuration and no role store has
            // been defined so don't spend cycles looking
        //    return null;
        //}

        ResultSet rs = null;
        PreparedStatement stmt = null;
        ResultSet rsr = null;
        PreparedStatement stmtr = null;
        AuthInfo rtn = new AuthInfo();

        try {
            stmt = ps(dbConnection, username);
            rs = stmt.executeQuery();

            //get user info
            if (rs.next()) {
            	rtn.setDbLoginName(rs.getString(1));
            	rtn.setDbCredentials(rs.getString(2));
            	rtn.setDbDisplayName(rs.getString(3));
                
            	if (jaasRoleNameCol!=null && jaasRoleNameCol.length()>0) {
	                String role = rs.getString(4);
	                if (role != null && role.length()>0) {
	                	ArrayList<String> dbJaasRoles = new ArrayList<String>();
	                	dbJaasRoles.add(role.trim());
	                	rtn.setDbJaasRoles(dbJaasRoles);
	                }
            	}
            	if (tenantIdCol!=null && tenantIdCol.length()>0) {
	                rtn.setDbTenantId(rs.getString(5));
            	}
            }
            
            //get biz role info
            if (bizRoleSqlSelector!=null) {
            	stmtr = psRoleSql(dbConnection, rtn.getDbLoginName());
            	rsr = stmtr.executeQuery();
            	ArrayList<String> dbBizRoles = new ArrayList<String>();
            	while (rsr.next()) {
                	String bizRoleId = rsr.getString(1);
                	if (bizRoleId != null && bizRoleId.length()>0) {
                		dbBizRoles.add(bizRoleId.trim());
	                }
                }
            	rtn.setDbBizRoles(dbBizRoles);
            }
            
            
            return rtn;
            
        } catch(SQLException e) {
            containerLog.error(
                sm.getString("dataSourceRealm.getRoles.exception", username), e);
            return null;
        }
        finally {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (stmt != null) {
                    stmt.close();
                }
                if (rsr != null) {
                    rsr.close();
                }
                if (stmtr != null) {
                    stmtr.close();
                }
            } catch (SQLException e) {
                    containerLog.error(
                        sm.getString("dataSourceRealm.getAuthInfo.exception",
                                     username), e);
            }
        }

    }

    /**
     * Return a PreparedStatement configured to perform the SELECT required
     * to retrieve user roles for the specified username.
     *
     * @param dbConnection The database connection to be used
     * @param username Username for which roles should be retrieved
     *
     * @exception SQLException if a database error occurs
     */
    private PreparedStatement ps(Connection dbConnection, String username)
        throws SQLException {

        PreparedStatement ps =
            dbConnection.prepareStatement(preparedSql);

        ps.setString(1, username);
        if (this.userNameCol1!=null) {
        	ps.setString(2, username);
        }
        if (this.userNameCol2!=null) {
        	ps.setString(3, username);
        }
        if (this.statusCol!=null && this.status!=null) {
            ps.setString(4, status);
        }
        return (ps);

    }
    
    private PreparedStatement psRoleSql(Connection dbConnection, String loginname)
        throws SQLException {
    	
    	String sql = bizRoleSqlSelector.getRoleSelectSql();
        PreparedStatement ps =
            dbConnection.prepareStatement(sql);

        int count = 1;
		int idx = sql.indexOf('?');
		while (idx>=0) {
			ps.setString(count++, loginname);
			idx = sql.indexOf('?', idx+1);
		}
        
        return (ps);

    }

    // ------------------------------------------------------ Lifecycle Methods

    /**
     * Prepare for the beginning of active use of the public methods of this
     * component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected void startInternal() throws LifecycleException {

        // Create the roles PreparedStatement string
        StringBuilder temp = new StringBuilder("SELECT ");
        temp.append(loginNameCol);
        temp.append(",");
        temp.append(userCredCol);
        temp.append(",");
    	temp.append(displayNameCol);
        if (jaasRoleNameCol!=null && jaasRoleNameCol.length()>0) {
        	temp.append(",");
        	temp.append(jaasRoleNameCol);
        } else {
        	temp.append(",'ROLE'");//fake column
        }
        if (tenantIdCol!=null && tenantIdCol.length()>0) {
        	temp.append(",");
        	temp.append(tenantIdCol);
        } else {
        	temp.append(",'TENANTID'");//fake column
        }
        temp.append(" FROM ");
        temp.append(userTable);
        temp.append(" WHERE (upper(");
        temp.append(loginNameCol);
        temp.append(") = upper(?)");
        if (userNameCol1!=null) {
        	temp.append(" OR upper(");
        	temp.append(userNameCol1);
            temp.append(") = upper(?)");
        }
        if (userNameCol2!=null) {
        	temp.append(" OR upper(");
        	temp.append(userNameCol2);
            temp.append(") = upper(?))");
        }
        if (statusCol!=null && status!=null) {
            temp.append(" AND "+statusCol+"=?");
        }
        
        preparedSql = temp.toString();
        
        if (bizRoleSqlSelectorClass!=null && bizRoleSqlSelectorClass.length()>0) {
        	try {
				this.bizRoleSqlSelector = (BizRoleSqlSelector)(Class.forName(bizRoleSqlSelectorClass).newInstance());
			} catch (Exception e) {
				e.printStackTrace();
				containerLog.error(
                        sm.getString("dataSourceRealm.startInternal.exception.class.newInstance",
                        		bizRoleSqlSelectorClass), e);
			}
        }

        super.startInternal();
    }
    
    private class AuthInfo {
    	String dbLoginName;
        String dbCredentials;
        String dbTenantId;
        String dbDisplayName;
        ArrayList<String> dbJaasRoles;
        ArrayList<String> dbBizRoles;
        
		public String getDbTenantId() {
			return this.dbTenantId;
		}
		public void setDbTenantId(String dbTenantId) {
			this.dbTenantId = dbTenantId;
		}
        public String getDbLoginName() {
			return dbLoginName;
		}
		public void setDbLoginName(String dbLoginName) {
			this.dbLoginName = dbLoginName;
		}
		public String getDbCredentials() {
			return dbCredentials;
		}
		public void setDbCredentials(String dbCredentials) {
			this.dbCredentials = dbCredentials;
		}
		public String getDbDisplayName() {
			return dbDisplayName;
		}
		public void setDbDisplayName(String dbDisplayName) {
			this.dbDisplayName = dbDisplayName;
		}
		public ArrayList<String> getDbJaasRoles() {
			return this.dbJaasRoles;
		}
		public void setDbJaasRoles(ArrayList<String> dbJaasRoles) {
			this.dbJaasRoles = dbJaasRoles;
		}
		public ArrayList<String> getDbBizRoles() {
			return dbBizRoles;
		}
		public void setDbBizRoles(ArrayList<String> dbBizRoles) {
			this.dbBizRoles = dbBizRoles;
		}
    }

	@Override
	protected String getPassword(String username) {
		return null;
	}

	@Override
	protected Principal getPrincipal(String username) {
		return null;
	}
}
